package com.xw.unification.handler;

import com.xw.unification.annotations.NativeResponse;
import com.xw.unification.annotations.ResultMessageMapping;
import com.xw.unification.annotations.ResultMessageMappings;
import com.xw.unification.annotations.UnifiedResponse;
import com.xw.unification.config.WebUnificationMvcProperties;
import com.xw.unification.config.WebUnificationMvcUnifiedExceptionConfig.ExceptionHandlerMethodDelegate;
import com.xw.unification.dto.ResultDTO;
import com.xw.unification.entity.ErrorBody;
import com.xw.unification.entity.ResultCodeEnumEmptyResolver;
import com.xw.unification.enums.ErrorStatusBodyStrategy;
import com.xw.unification.enums.MessagePriority;
import com.xw.unification.interfaces.ResultCodeEnumResolvable;
import com.xw.unification.interfaces.ResultCodeEnumerable;
import com.xw.unification.utils.AnnotationDetector;
import com.xw.unification.helper.StringHelper;
import com.xw.unification.utils.JacksonUtils;
import lombok.ToString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

public class UnifiedResponseHandler implements ResponseBodyAdvice<Object> {

    private static final Logger LOGGER = LoggerFactory.getLogger(UnifiedResponseHandler.class);

    @SuppressWarnings("all")
    private final ResultCodeEnumResolvable<? extends Enum<?>> resolver;

    private final WebUnificationMvcProperties.UnifiedResponseProperties properties;

    private final Map<Class<?>, Map<Integer, String>> RESULT_MESSAGE_CLASS_MAPPING_CACHE = new ConcurrentHashMap<>();

    private final Map<Method, Map<Integer, String>> RESULT_MESSAGE_MAPPING_CACHE = new ConcurrentHashMap<>();

    /**
     * spring http 标准错误码 - 自定义错误码枚举 映射, 将 {@link org.springframework.http.HttpStatus} 中定义的 status
     */
    private final Map<Integer, ResultCodeEnumerable> errCodeMapping;
    // add base packages properties for unified response module to avoid the question from some special requests which have mistakes
    // when it was handled by ResponseBodyAdvice with RestControllerAdvice annotation,
    // such as swagger request

    /**
     * 全局错误码 Message 映射
     */
    private final Map<Integer, String> errCodeMessageMapping;

    public UnifiedResponseHandler(
            ResultCodeEnumResolvable<?> resultCodeEnumResolver,
            WebUnificationMvcProperties webUnificationMvcProperties
    ) {
        resolver = resultCodeEnumResolver;
        properties = webUnificationMvcProperties.getUnifiedResponse();
        Map<Integer, String> statusMapping = new HashMap<>(properties.getResult().getErrorStatusEnumMapping());
        Map<String, ResultCodeEnumerable> enumNameMapping = new HashMap<>(statusMapping.size());
        Map<Integer, ResultCodeEnumerable> mapping = new HashMap<>(statusMapping.size());
        String nameStr;
        ResultCodeEnumerable codeEnumerable;
        if (resolver != null && !(resolver instanceof ResultCodeEnumEmptyResolver)
                && !statusMapping.isEmpty()) {
            for (Integer code : statusMapping.keySet()) {
                if (code == null) {
                    continue;
                }
                nameStr = statusMapping.get(code);
                if (!StringHelper.hasText(nameStr)) {
                    continue;
                }
                codeEnumerable = enumNameMapping.get(nameStr);
                if (codeEnumerable == null) {
                    Enum<?> enumerable = resolver.nameOf(nameStr);
                    if (enumerable instanceof ResultCodeEnumerable) {
                        codeEnumerable = (ResultCodeEnumerable) enumerable;
                    }
                }
                if (codeEnumerable != null) {
                    mapping.put(code, codeEnumerable);
                    enumNameMapping.put(nameStr, codeEnumerable);
                }
            }
        }
        errCodeMapping = mapping;
        Map<Integer, String> messageMapping = properties.getResult().getResultMessageGlobalMapping();
        if (messageMapping != null && !messageMapping.isEmpty()) {
            errCodeMessageMapping = new HashMap<>(messageMapping);
        } else {
            errCodeMessageMapping = Collections.emptyMap();
        }
        LOGGER.info("[WebUnificationMvcUnifiedResponseConfig] ### UnifiedResponseHandler has bean initialized");
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        Method method = returnType.getMethod();
        List<String> basePackages = getBasePackages();
        if (basePackages != null && !basePackages.isEmpty() && method != null) {
            boolean inPackage = false;
            String pkgName = method.getDeclaringClass().getPackage().getName();
            for (String basePackage : basePackages) {
                if (basePackage != null && pkgName != null && pkgName.startsWith(basePackage)) {
                    inPackage = true;
                    break;
                }
            }
            if (!inPackage) {
                return false;
            }
        }
        return method != null &&
                AnnotationDetector.findAnnotation(method, NativeResponse.class) == null
                && AnnotationDetector.findAnnotation(method.getDeclaringClass(), NativeResponse.class) == null
                ;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        Method method = returnType.getMethod();
        if (method == null || !(response instanceof ServletServerHttpResponse)) {
            return body;
        }
        if (body instanceof ResultDTO) {
            return overwriteErrMessageByMapping((ResultDTO<?>) body, method);
        }
        UnifiedResponse unifiedResponseAnno = AnnotationDetector.findAnnotation(method, UnifiedResponse.class);
        HttpServletResponse resp = ((ServletServerHttpResponse) response).getServletResponse();
        HttpHeaders headers = response.getHeaders();
        MediaType originalContentType = headers.getContentType();
        try {
            String defaultContentType = properties.isAggressiveConvertContentTypeToJson() ?
                    MediaType.APPLICATION_JSON_VALUE : (
                    unifiedResponseAnno == null ?
                            properties.getDefaultContentType() : unifiedResponseAnno.contentType()
            );
            headers.setContentType(MediaType.parseMediaType(defaultContentType));
            if (unifiedResponseAnno != null) {
                resp.setContentType(unifiedResponseAnno.contentType());
            }
            headers.setContentType(MediaType.parseMediaType(defaultContentType));
        } catch (Exception e) {
            headers.setContentType(originalContentType);
        }
        resp.setCharacterEncoding(properties.getDefaultCharacterEncoding());
        int status = resp.getStatus();
        ResultCodeEnumerable codeEnumerable = null;
        if (status >= HttpStatus.BAD_REQUEST.value()) {
            resp.setStatus(HttpStatus.OK.value());
            // 获取 HttpStatus 错误码映射自定义枚举
            codeEnumerable = errCodeMapping.get(status);
        }
        Object result;
        if (codeEnumerable != null) {
            result = wrapResult(resp, method, unifiedResponseAnno, codeEnumerable, convertErrorBodyByProperties(status, body), true);
        } else if (status >= HttpStatus.BAD_REQUEST.value()) {
            result = wrapResult(resp, method, unifiedResponseAnno, null, convertErrorBodyByProperties(status, body), false);
        } else {
            result = wrapResult(resp, method, unifiedResponseAnno, null, body, true);
        }
        if (!MappingJackson2HttpMessageConverter.class.isAssignableFrom(selectedConverterType)) {
            return JacksonUtils.json(result);
        } else {
            return result;
        }
    }

    protected List<String> getBasePackages() {
        return Collections.emptyList();
    }

    private Object wrapResult(HttpServletResponse resp, Method method, UnifiedResponse unifiedResponseAnno, ResultCodeEnumerable codeEnumerable, Object body, boolean success) {

        if (unifiedResponseAnno != null) {
            resp.setCharacterEncoding(unifiedResponseAnno.characterEncoding());
            resp.setContentType(unifiedResponseAnno.contentType());
            if (unifiedResponseAnno.nativeBody()) {
                return body;
            }
        }

        int errorCode = !success ? ResultCodeEnumerable.DEFAULT.getErrorCode() : (codeEnumerable == null ?
                ResultCodeEnumerable.DEFAULT.getSuccessCode() : codeEnumerable.getCode());
        String message = !success ? ResultCodeEnumerable.DEFAULT.getErrorMsg() : (codeEnumerable == null ?
                ResultCodeEnumerable.DEFAULT.getSuccessMsg() : codeEnumerable.getMessage());

        ResultDTO<Object> resultDTO = InnerResultDTO.customize(errorCode, message, body);
        return overwriteErrMessageByMapping(resultDTO, method);
    }

    private Object convertErrorBodyByProperties(int status, Object body) {
        ErrorStatusBodyStrategy strategy = properties.getResult().getErrorStatusBodyStrategy();
        strategy = Optional.ofNullable(strategy).orElse(ErrorStatusBodyStrategy.NONE);
        switch (strategy) {
            case NONE:
                return null;
            case BODY:
                return body;
            case MESSAGE:
                try {
                    if (body instanceof ErrorBody) {
                        ErrorBody errorBody = (ErrorBody) body;
                        return StringHelper.hasText(errorBody.getMessage()) ? errorBody.getMessage() : errorBody.getError();
                    }
                    if (body instanceof LinkedHashMap) {
                        LinkedHashMap<String, Object> bodyMap = (LinkedHashMap<String, Object>) body;
                        Object message = bodyMap.getOrDefault("message", "");
                        if (message instanceof String && StringHelper.hasText((String) message)) {
                            return message;
                        }
                    }
                    return HttpStatus.valueOf(status).getReasonPhrase();
                } catch (Exception e) {
                    return null;
                }
            default:
                return null;
        }
    }

    /**
     * 根据 resultCode - Message 映射 替换 dto 中的 message
     *
     * @param dto       统一响应体
     * @param method    方法
     * @return ResultDTO
     */
    private ResultDTO<?> overwriteErrMessageByMapping(ResultDTO<?> dto, @SuppressWarnings("unused") Method method) {
        Map<Integer, String> map = getResultMessageMappingMap(method);
        if (!map.isEmpty()) {
            String message = map.get(dto.getCode());
            if (message != null) {
                dto.setMessage(message);
                return dto;
            }
        }
        // 如果 errCodeMessageMapping 映射中有对应 code 的映射, 则替换 message
        if (!errCodeMessageMapping.isEmpty() && errCodeMessageMapping.containsKey(dto.getCode())) {
            dto.setMessage(errCodeMessageMapping.get(dto.getCode()));
        }
        return dto;
    }

    private Map<Integer, String> getResultMessageMappingMap(Method method) {
        if (method == null) {
            return Collections.emptyMap();
        }
        // 如果是异常处理后的返回体, 则通过 ExceptionHandlerMethodDelegate 代理类获取原始处理方法
        if (UnifiedExceptionHandler.class.isAssignableFrom(method.getDeclaringClass())) {
            if (properties.getResult().getResultMessagePriorityWhenUnifiedException() == MessagePriority.ANNOTATION) {
                HandlerMethod handlerMethod = ExceptionHandlerMethodDelegate.getHandlerMethod();
                if (handlerMethod == null || handlerMethod.getMethod() == null) {
                    return Collections.emptyMap();
                }
                method = handlerMethod.getMethod();
            } else {
                return Collections.emptyMap();
            }
        }
        Map<Integer, String> map = RESULT_MESSAGE_MAPPING_CACHE.get(method);
        if (map == null) {
            ResultMessageMapping methodMapping = AnnotationDetector.findAnnotation(method, ResultMessageMapping.class);
            ResultMessageMappings methodMappings = AnnotationDetector.findAnnotation(method, ResultMessageMappings.class);
            Class<?> declaringClass = method.getDeclaringClass();
            Map<Integer, String> classMappingCache = RESULT_MESSAGE_CLASS_MAPPING_CACHE.get(declaringClass);
            if (classMappingCache == null) {
                synchronized (RESULT_MESSAGE_CLASS_MAPPING_CACHE) {
                    classMappingCache = RESULT_MESSAGE_CLASS_MAPPING_CACHE.get(declaringClass);
                    if (classMappingCache == null) {
                        classMappingCache = new HashMap<>();
                        ResultMessageMapping classMapping = AnnotationDetector.findAnnotation(declaringClass, ResultMessageMapping.class);
                        ResultMessageMappings classMappings = AnnotationDetector.findAnnotation(declaringClass, ResultMessageMappings.class);
                        if (classMappings != null) {
                            ResultMessageMapping[] value = classMappings.value();
                            for (ResultMessageMapping mapping : value) {
                                if (mapping != null) {
                                    classMappingCache.put(mapping.code(), mapping.message());
                                }
                            }
                        }
                        if (classMapping != null) {
                            classMappingCache.put(classMapping.code(), classMapping.message());
                        }
                        RESULT_MESSAGE_CLASS_MAPPING_CACHE.put(declaringClass, classMappingCache);
                    }
                }
            }
            if (methodMapping == null && methodMappings == null && classMappingCache.isEmpty()) {
                map = Collections.emptyMap();
                RESULT_MESSAGE_MAPPING_CACHE.put(method, map);
            } else {
                synchronized (RESULT_MESSAGE_MAPPING_CACHE) {
                    map = RESULT_MESSAGE_MAPPING_CACHE.get(method);
                    if (map != null) {
                        return map;
                    }
                    List<ResultMessageMapping> mappingList;
                    if (methodMappings == null) {
                        mappingList = new ArrayList<>();
                    } else {
                        mappingList = new ArrayList<>(Arrays.asList(methodMappings.value()));
                    }
                    if (methodMapping != null) {
                        mappingList.add(methodMapping);
                    }
                    map = new HashMap<>(classMappingCache);
                    for (ResultMessageMapping mapping : mappingList) {
                        if (mapping != null) {
                            map.put(mapping.code(), mapping.message());
                        }
                    }
                    RESULT_MESSAGE_MAPPING_CACHE.put(method, map);
                }
            }
        }
        return map;
    }


    /**
     * 内部统一响应体, 仅为包内自定义响应体使用
     *
     * @param <V>   响应数据体泛型
     */
    @ToString
    public static final class InnerResultDTO<V> extends ResultDTO<V> {

        private InnerResultDTO(int code, String message, V data) {
            super(code, message, data);
        }

        public static <V> ResultDTO<V> customize(int code, String message, V data) {
            return ResultDTO.customize(code, message, data);
        }

    }

}
